import { gitcmd } from './cmd'
import findGit from './findGit'
import { GitLocation } from './locator'

export const EOL_REGEX = /\r\n|\r|\n/g
export const INVALID_BRANCH_REGEXP = /^\(.* .*\)$/
export const REMOTE_HEAD_BRANCH_REGEXP = /^remotes\/.*\/HEAD$/

export interface GitBranches {
  branches: string[]
  head: string | null
}

export interface GitSampleLogItem {
  commit: string
  desc: string
}

export interface GitSampleLog extends Array<GitSampleLogItem> {}

export interface GitFilesFromCommitsOptions {
  commitIdStart: string
  commitIdEnd: string
  author?: string
  authors?: string[]
}

export interface GitCommitRangeOptions {
  commitIdStart: string
  commitIdEnd: string
}

export interface GitFileContentFromCommitOptions {
  commit: string
  filePath: string
}

export interface GitFileLogFromCommitRangeOptions {
  commitIdStart: string
  commitIdEnd: string
  filePath: string
}

export interface GitFileLogFromCommitOptions {
  commit: string
  filePath: string
}

export interface GitChangedFile {
  srcFilePath?: string // only exists for C or R.
  filePath: string
  actionFlag: string
  addedLines: string
  removedLines: string
}

export default class Git {
  cwd: string
  getGitLocation: () => Promise<GitLocation>

  constructor(cwd: string) {
    this.cwd = cwd
    const gitLocationPromise = findGit()
    this.getGitLocation = () => gitLocationPromise
  }

  async cmd(...args: string[]) {
    const gitLocation = await this.getGitLocation()
    return gitcmd<string>({ cwd: this.cwd }, gitLocation, ...args)
  }

  async getBranches() {
    const stdout = await this.cmd('branch', '-a', '--no-color')

    let branchData: GitBranches = { branches: [], head: null }
    let lines = stdout.split(EOL_REGEX)
    for (let i = 0; i < lines.length - 1; i++) {
      let name = lines[i].substring(2).split(' -> ')[0]
      if (INVALID_BRANCH_REGEXP.test(name) || REMOTE_HEAD_BRANCH_REGEXP.test(name)) {
        continue
      }

      if (lines[i][0] === '*') {
        branchData.head = name
        branchData.branches.unshift(name)
      } else {
        branchData.branches.push(name)
      }
    }
    return branchData
  }

  async getFilesFromCommits(opts: GitFilesFromCommitsOptions) {
    const args = []
    if (opts.author) args.push(`--author=${opts.author}`)
    if (opts.authors) opts.authors.forEach((v) => args.push(`--author=${v}`))
    args.push(
      '--format=%x00%an%x00%ae%x00%H',
      '--name-only',
      `${opts.commitIdStart}..${opts.commitIdEnd}`
    )
    const stdout = await this.cmd('log', ...args)

    const map = new Map<string, Map<string, { hash: string }[]>>()

    const REG = /\x00([^\x00]+\x00[^\x00]+)\x00([^\x00]+)\n\n/g
    let matchList = REG.exec(stdout)
    do {
      if (!matchList) break
      const [, authorAEmail, hash] = matchList
      const startIndex = REG.lastIndex
      matchList = REG.exec(stdout)
      const endIndex = matchList ? REG.lastIndex - matchList[0].length : stdout.length
      const contentLines = stdout.slice(startIndex, endIndex).split(EOL_REGEX)
      contentLines.forEach((v) => {
        let s = map.get(v)
        if (!s) map.set(v, (s = new Map()))
        let a = s.get(authorAEmail)
        if (!a) s.set(authorAEmail, (a = []))
        a.push({ hash })
      })
    } while (true)
    const resMap = new Map<string, { name: string; email: string; commits: { hash: string }[] }[]>()
    for (const [k, v] of map) {
      const list = [...v].map(([authorAEmail, commits]) => {
        const [name, email] = authorAEmail.split('\x00', 2)
        return { name, email, commits }
      })
      resMap.set(k, list)
      map.delete(k)
    }
    return resMap
  }

  async getFilesDiffFromCommits(opts: GitCommitRangeOptions) {
    const stdout = await this.cmd(
      'diff-tree',
      '-r',
      '--raw',
      '-C50%',
      '-M50%',
      '--numstat',
      '-z',
      opts.commitIdStart,
      opts.commitIdEnd,
      '--'
    )
    const RAW_REG =
      /:(\w{6}) (\w{6}) (\w{40}) (\w{40}) (\w)(\d+)?\x00(?:([^\x00:\t]+)\x00)?([^\x00:\t]+)\x00/g
    const NUMSTAT_REG = /(\d+|-)\t(\d+|-)\t(?:\x00([^\x00]+)\x00)?([^\x00]+)\x00/g
    const res = new Map<string, GitChangedFile>()
    do {
      const a = RAW_REG.exec(stdout)
      if (!a) break
      const [, srcMode, dstMode, srcHash, dstHash, actionFlag, score, srcFilePath, filePath] = a
      res.set(filePath, {
        srcFilePath,
        filePath,
        actionFlag,
        addedLines: '-',
        removedLines: '-',
      })
      if (stdout[RAW_REG.lastIndex] !== ':') {
        NUMSTAT_REG.lastIndex = RAW_REG.lastIndex
        break
      }
    } while (true)
    do {
      const a = NUMSTAT_REG.exec(stdout)
      if (!a) break
      const [, addedLines, removedLines, srcFilePath, filePath] = a
      const b = res.get(filePath)
      if (b) {
        b.addedLines = addedLines
        b.removedLines = removedLines
      } else {
        console.warn(`not found ${filePath}`)
      }
    } while (true)
    return res
  }

  async getFileContentFromCommit(opts: GitFileContentFromCommitOptions) {
    return await this.cmd('show', `${opts.commit}:${opts.filePath}`)
  }

  async getCommitsFromCommits(opts: GitFilesFromCommitsOptions) {
    const args = []
    if (opts.author) args.push(`--author=${opts.author}`)
    if (opts.authors) opts.authors.forEach((v) => args.push(`--author=${v}`))
    args.push('--pretty=format:"%H"', `${opts.commitIdStart}..${opts.commitIdEnd}`)
    const stdout = await this.cmd('log', ...args)
    const set = new Set(
      stdout
        .replace(/['"]/g, '')
        .split(EOL_REGEX)
        .filter((v) => v)
    )
    return set
  }

  async getBlameFile(opts: GitFileContentFromCommitOptions) {
    const stdout = await this.cmd('blame', opts.commit, '--', opts.filePath)
    return stdout.split(EOL_REGEX)
  }

  async getGitRoot() {
    return await this.cmd('rev-parse', '--show-toplevel')
  }
}
